package javango.contrib.servlet;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javango.api.Settings;
import javango.core.AnonymousUser;
import javango.core.JavangoLifecycleListener;
import javango.core.servers.RequestProcessor;
import javango.http.Http404;
import javango.http.HttpException;
import javango.http.HttpRequest;
import javango.http.HttpResponse;
import javango.http.SimpleHttpResponse;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.inject.Binder;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.name.Named;

public class Servlet extends HttpServlet implements Module, javango.core.JavangoLifecycle {
	
	public static final String BROKEN_LINK_LOG_LEVEL = "BROKEN_LINK_LOG_LEVEL";

	/**
	 * 
	 */
	private static final long serialVersionUID = 4173911300243443940L;

	private final static Log log = LogFactory.getLog(Servlet.class);
	
	Injector injector;
	
	// TODO Should Guice provide this giving the developer the flexability to create a new one or use a singleton
	// therefore getting rid of this property?
	@Inject
	RequestProcessor requestProcessor;
	
	@Inject(optional=true)
	@Named("javango.characterEncoding")
	String characterEncoding;
	
	List<JavangoLifecycleListener> listeners = new ArrayList<JavangoLifecycleListener>();
	
	protected Injector getInjector() throws ServletException {
		if (injector != null) {
			return injector;
		}
		try {

            String guiceModule = this.getServletConfig().getInitParameter("GuiceModules");
            String settings = this.getServletConfig().getInitParameter("Settings");

            List<Module> moduleList = new ArrayList<Module>();
            moduleList.add(this);

            if (!StringUtils.isBlank(guiceModule)) {
            	for (String moduleName : guiceModule.split(";")) {
            		moduleList.add((Module)Class.forName(moduleName.trim()).newInstance());
            	}
        		return  com.google.inject.Guice.createInjector(moduleList);
            } else if (!StringUtils.isBlank(settings)) {
           		return ((Settings)Class.forName(settings.trim()).newInstance()).createInjector(moduleList);
            } else {
            	throw new ServletException("You must provide GuiceModules or Settings as a parameter to the servlet");
            }
            
		} catch (Exception e) {
			// TODO Instead of throwing servletexception add a single url (^.*$) to the list that shows the error message and stack.
			log.error(e,e);
			throw new ServletException(e);
		}
	}
	
	@Override
	public void init() throws ServletException {
		log.debug("Entering Servlet.init");
		super.init();
		injector = getInjector();
		injector.injectMembers(this);

//        requestProcessor = injector.getInstance(RequestProcessor.class);      
//        String charEncode = injector.getInstance(arg0)
        
        for(JavangoLifecycleListener listener: listeners) {
        	listener.start();
        }
	}

	
	public void configure(Binder binder) {
		binder.bind(Servlet.class).toInstance(this);
//		binder.bind(RegUrlFactory.class).toProvider(FactoryProvider.newFactory(RegUrlFactory.class, RegUrl.class));
	}

	public void addListener(JavangoLifecycleListener listener) {
		this.listeners.add(listener);		
	}

	@Override
	public void destroy() {
		requestProcessor.stop();
		
        for(JavangoLifecycleListener listener: listeners) {
        	listener.stop();
        }
        
		LogFactory.releaseAll();
		super.destroy();
	}

	public void service(final HttpServletRequest servletRequest, final HttpServletResponse servletResponse) throws IOException, ServletException {
		if (characterEncoding != null) {
			servletRequest.setCharacterEncoding(characterEncoding);
			servletResponse.setCharacterEncoding(characterEncoding);
		}
		HttpRequest request = injector.getInstance(HttpRequest.class);
		
		String context = StringUtils.trimToEmpty(servletRequest.getContextPath());
		String path = StringUtils.trimToEmpty(servletRequest.getRequestURI());

		try {			
			// TODO context should always end in a trailing slash and path should always start without a slash.
			
			path = path.substring(context.length()+1); // +1 to gobble up the trailing slash
			request.setPath(path);
			request.setContext(context);
			request.setMethod(servletRequest.getMethod());
			request.setSession(new HttpServletSession(servletRequest));
			
			// Check that we have a file upload request
			boolean isMultipart = ServletFileUpload.isMultipartContent(servletRequest);
			if (isMultipart) {
				Map<String, FileItem> files = new HashMap<String, FileItem>();
				Map<String, String[]> parameters = new HashMap<String, String[]>();
				
				// Create a factory for disk-based file items
				FileItemFactory factory = new DiskFileItemFactory();

				// Create a new file upload handler
				ServletFileUpload upload = new ServletFileUpload(factory);

				// Parse the request
				List<FileItem> items = upload.parseRequest(servletRequest);

				// Process the uploaded items
				Iterator<FileItem> iter = items.iterator();
				while (iter.hasNext()) {
				    FileItem item = (FileItem) iter.next();

				    if (item.isFormField()) {
				        parameters.put(item.getFieldName(), new String[] {item.getString()}); // TODO Allow multiple values,  check to see if parameters already has a value with this name and append.
				    } else {
				        files.put(item.getFieldName(), item);
				    }
				}
				request.setFiles(files);
				request.setParameterMap(parameters);

			} else {
				request.setParameterMap(servletRequest.getParameterMap());
				request.setFiles(null);
			}
			
			if (servletRequest.getUserPrincipal() == null) {
				// TODO this user should not necessarly change from request to request (maybe).  only works if the user has cookies,  see how django handles.
				request.setUser(new AnonymousUser());
			} else {
				request.setUser(new PrincipalUser(servletRequest));
			}
			
			HttpResponse response = requestProcessor.service(request);
			response.render(servletRequest, servletResponse);
			
			// tell the request that we are done so it can notify any listeners.  good place to close database sessions, etc. 
			request.close();
		} catch (Http404 e) {
			String level = injector.getInstance(Settings.class).get(BROKEN_LINK_LOG_LEVEL);
			if (level != null) {
				if ("trace".equalsIgnoreCase(level)) {
					log.trace(e,e);
				} else if ("debug".equalsIgnoreCase(level)) {
					log.debug(e,e);
				} else if ("info".equalsIgnoreCase(level)) {
					log.info(e,e);
				} else if ("warn".equalsIgnoreCase(level)) {
					log.warn(e,e);
				} else if ("error".equalsIgnoreCase(level)) {
					log.error(e,e);
				} else if ("fatal".equalsIgnoreCase(level)) {
					log.fatal(e,e);
				}
			}
			if (!injector.getInstance(Settings.class).isDebug()) {
				// TODO ability to specify 404 handler
				SimpleHttpResponse response = new SimpleHttpResponse("Page not found");
				response.setStatusCode(404);
				try {
					response.render(servletRequest, servletResponse);
				} catch (HttpException e2) {
					log.error(e2,e2);
					throw new ServletException(e);
				}
			} else {
				throw new ServletException(e);
			}
		} catch (FileUploadException e) {
			log.error(e,e);
			try {
				request.close();
			} catch (HttpException e1) {
				log.error(e1,e1);
			}
			// TODO Pretty print the exception and stack trace,  request information, etc
			if (!injector.getInstance(Settings.class).isDebug()) {
				HttpResponse response = new SimpleHttpResponse("Unrecoverable error processing uploaded files, please contact support");
				try {
					response.render(servletRequest, servletResponse);
				} catch (HttpException e2) {
					log.error(e2,e2);
					throw new ServletException(e);
				}
			}
		} catch (HttpException e) {
			log.error(String.format("HttpException while processing for user '%s' %s:%s", request.getUser(), context, path));
			log.error(e,e);
			try {
				request.close();
			} catch (HttpException e1) {
				log.error(e1,e1);
			}
			// TODO Pretty print the exception and stack trace,  request information, etc
			// TODO can this exception be handled by middleware?  middleware is not really avaliable here as it is handled in the requestProcessor...
			if (!injector.getInstance(Settings.class).isDebug()) {
				HttpResponse response = new SimpleHttpResponse("Unrecoverable error, please contact support");
				try {
					response.render(servletRequest, servletResponse);
				} catch (HttpException e2) {
					log.error(e2,e2);
					throw new ServletException(e);
				}
			} else {
				throw new ServletException(e);
			}
		}
	}
	
}
